/*******************************************************************************
 * Copyright 2011 Mauro Luigi Drago (drago.luigi@gmail.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *******************************************************************************/
package com.bytenose.extrautils.patterns.dispatcher;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import com.bytenose.extrautils.annotations.NonNullable;
import com.bytenose.extrautils.caching.ICache;
import com.bytenose.extrautils.caching.WeakCache;
import com.bytenose.extrautils.logging.ILogger;
import com.bytenose.extrautils.logging.Loggers;

import com.google.common.base.*;
import com.google.common.collect.*;

/**
 * A polymorphic, lazy, single method dispatcher which uses caching to improve
 * performance. Methods are looked up only when invocation occurs, i.e., lazy
 * lookup computation is performed, and such information is cached. Methods are
 * looked up according to the type of the receiving object, which is specified
 * at invocation time.
 * 
 * @author Mauro Luigi Drago
 * 
 * @param <RT>
 *            the return type of the method.
 * 
 */
public final class MethodDispatcher<RT> implements IDispatcher<RT> {
	/** The filter to select methods */
	private final Predicate<Method> methodsFilter;

	/** The return type of the method */
	private final Class<RT> returnType;

	/** The set of methods to dispatch invocations */
	private final ICache<Class<?>, ImmutableSortedSet<Method>> lookupInfo = WeakCache
			.newInstance();

	/** The event logger */
	private final ILogger logger;

	/** The handler to take control over method selection */
	private final IMethodSelectionHandler msHandler = DEFAULT_SELECTION_HANDLER;

	/**
	 * Creates a new {@link MethodDispatcher}.
	 * 
	 * @param methodsFilter
	 *            the filter to select methods for dispatching, cannot be null.
	 * @param returnType
	 *            the type of the return value, cannot be null.
	 * @param logger
	 *            the event logger, can be null.
	 * @throws IllegalArgumentException
	 *             if some of the {@link NonNullable} arguments are null.
	 */
	private MethodDispatcher(@NonNullable Predicate<Method> methodsFilter,
			@NonNullable Class<RT> returnType, ILogger logger)
			throws IllegalArgumentException {
		if (methodsFilter == null)
			throw new IllegalArgumentException("methodsFilter");
		if (returnType == null)
			throw new IllegalArgumentException("returnType");
		this.methodsFilter = methodsFilter;
		this.returnType = returnType;
		this.logger = logger == null ? Loggers.NULL_LOGGER : logger;
	}

	/**
	 * Creates a new {@link MethodDispatcher} for methods with a specified name
	 * and return type.
	 * 
	 * @param <RT>
	 *            the return type of the method.
	 * @param methodName
	 *            the name of the methods to select for dispatching, cannot be
	 *            null.
	 * @param returnType
	 *            the return type of the methods to select for dispatching,
	 *            cannot be null.
	 * @return the new dispatcher.
	 * @throws IllegalArgumentException
	 *             if some of the {@link NonNullable} arguments are null.
	 */
	public static <RT> MethodDispatcher<RT> newInstance(
			@NonNullable String methodName, @NonNullable Class<RT> returnType)
			throws IllegalArgumentException {
		if (methodName == null)
			throw new IllegalArgumentException("methodName");
		if (returnType == null)
			throw new IllegalArgumentException("returnType");

		return new MethodDispatcher<RT>(Predicates.and(
				NameFilters.getSimpleNameFilter(methodName),
				ParamFilters.getReturnTypeFilter(returnType, false)),
				returnType, null);
	}

	/**
	 * Creates a new {@link MethodDispatcher} for methods with a specified name
	 * and void return type.
	 * 
	 * @param <RT>
	 *            the return type of the method.
	 * @param methodName
	 *            the name of the methods to select for dispatching, cannot be
	 *            null.
	 * @return the new dispatcher.
	 * @throws IllegalArgumentException
	 *             if some of the {@link NonNullable} arguments are null.
	 */
	public static MethodDispatcher<Void> newInstance(
			@NonNullable String methodName) throws IllegalArgumentException {
		if (methodName == null)
			throw new IllegalArgumentException("methodName");
		return new MethodDispatcher<Void>(Predicates.and(
				NameFilters.getSimpleNameFilter(methodName),
				ParamFilters.getVoidReturnFilter()), Void.TYPE, null);
	}

	/**
	 * Creates a new {@link MethodDispatcher} for methods selected by a
	 * specified methods filter.
	 * 
	 * @param <RT>
	 *            the return type of the methods.
	 * @param methodFilter
	 *            the filter to select methods for dispatching, cannot be null.
	 * @param returnType
	 *            the return type of the methods to select for dispatching,
	 *            cannot be null.
	 * @return the new dispatcher.
	 * @throws IllegalArgumentException
	 *             if some of the {@link NonNullable} arguments are null.
	 */
	public static <RT> MethodDispatcher<RT> newInstance(
			@NonNullable Predicate<Method> methodFilter,
			@NonNullable Class<RT> returnType) throws IllegalArgumentException {
		if (methodFilter == null)
			throw new IllegalArgumentException("methodFilter");
		if (returnType == null)
			throw new IllegalArgumentException("returnType");

		return new MethodDispatcher<RT>(Predicates.and(methodFilter,
				ParamFilters.getReturnTypeFilter(returnType, false)),
				returnType, null);
	}

	/**
	 * Creates a new {@link MethodDispatcher} for methods selected by a
	 * specified methods filter and void return type.
	 * 
	 * @param <RT>
	 *            the return type of the methods.
	 * @param methodFilter
	 *            the filter to select methods for dispatching, cannot be null.
	 * @param returnType
	 *            the return type of the methods to select for dispatching,
	 *            cannot be null.
	 * @return the new dispatcher.
	 * @throws IllegalArgumentException
	 *             if some of the {@link NonNullable} arguments are null.
	 */
	public static MethodDispatcher<Void> newInstance(
			@NonNullable Predicate<Method> methodFilter)
			throws IllegalArgumentException {
		if (methodFilter == null)
			throw new IllegalArgumentException("methodFilter");

		return new MethodDispatcher<Void>(Predicates.and(methodFilter,
				ParamFilters.getVoidReturnFilter()), Void.TYPE, null);
	}

	/**
	 * Lookups methods for a specific target class.
	 * 
	 * @param targetType
	 * @param params
	 * @return
	 */
	private Set<Method> lookupMethods(@NonNullable Class<?> targetType,
			@NonNullable List<Object> params) {
		// Check if lookup information has been already computed
		// for the specified target class
		if (!lookupInfo.containsKey(targetType))
			lookupInfo.put(targetType, computeLookupInfo(targetType));

		// Lookup the required method
		ImmutableSortedSet<Method> candidates = lookupInfo.get(targetType);
		return Sets.filter(candidates, getActualParamFilter(params));
	}

	/**
	 * Computes method lookup information for a specified type.
	 * 
	 * @param targetType
	 *            the target type, cannot be null.
	 * @return the set of methods candidate for invocation.
	 */
	private ImmutableSortedSet<Method> computeLookupInfo(
			@NonNullable Class<?> targetClass) {
		TreeSet<Method> orderedMethods = Sets
				.newTreeSet(new MethodComparator());
		Iterables
				.addAll(orderedMethods, Iterables.filter(
						Arrays.asList(targetClass.getDeclaredMethods()),
						methodsFilter));
		return ImmutableSortedSet.copyOfSorted(orderedMethods);
	}

	@Override
	public RT invoke(Object target, Object... params)
			throws IllegalArgumentException, InvocationTargetException,
			DispatchingException, NoSuchMethodException {
		if (target == null) throw new IllegalArgumentException("target");
		
		Set<Method> candidateMethods = lookupMethods(target.getClass(),
				Arrays.asList(params));
		if (candidateMethods.size() == 0) { // No methods found
			msHandler.noMethodFound();
		}

		if (candidateMethods.size() > 1) {
			if (msHandler.canHandleMultiMethods()) {
				// TODO implement me please
				throw new UnsupportedOperationException();
			} else { // Signal condition to the caller
				throw new DispatchingException(
						"Multiple candidate methods found, but dispatcher does not support multiple candidates.");
			}
		}
		
		Method m = candidateMethods.iterator().next();
		return invokeInternal(m, target, params);
	}

	@Override
	public RT invokeStatic(Class<?> target, Object... params) throws IllegalArgumentException,
			InvocationTargetException, DispatchingException,
			NoSuchMethodException {
		Set<Method> candidateMethods = lookupMethods(target, Arrays.asList(params));
		if (candidateMethods.size() == 0) { // No methods found
			msHandler.noMethodFound();
		}

		if (candidateMethods.size() > 1) {
			if (msHandler.canHandleMultiMethods()) {
				// TODO implement me please
				throw new UnsupportedOperationException(); 
			} else { // Signal condition to the caller
				throw new DispatchingException(
						"Multiple candidate methods found, but dispatcher does not support multiple candidates.");
			}
		}
		
		Method m = candidateMethods.iterator().next();
		return invokeInternal(m, target, params);
	}

	@Override
	public RT invokeSwallow(Object target, Object... params)
			throws IllegalArgumentException, InvocationTargetException {
		try {
			return invoke(target, params);
		} catch (DispatchingException e) {
			// Just log and exit
			logger.logError(e);
			return null;
		} catch (NoSuchMethodException e) {
			// Just log and exit
			logger.logError(e);
			return null;
		}
	}

	@Override
	public RT invokeStaticSwallow(Class<?> target, Object... params)
			throws IllegalArgumentException, InvocationTargetException {
		try {
			return invokeStatic(target, params);
		} catch (DispatchingException e) {
			// Just log and exit
			logger.logError(e);
			return null;
		} catch (NoSuchMethodException e) {
			// Just log and exit
			logger.logError(e);
			return null;
		}
	}

	private RT invokeInternal(Method method, Object target, Object... params)
			throws DispatchingException, InvocationTargetException {
		try {
			Object result = method.invoke(target, params);
			if (result == null)
				return null;
			if (returnType.isInstance(result))
				return returnType.cast(result);
			else {
				DispatchingException de = new DispatchingException(
						"Method result is not compatible with specified return type");
				logger.logError(de);
				throw de;
			}
		} catch (IllegalArgumentException e) {
			DispatchingException de = new DispatchingException(e);
			logger.logError(de);
			throw de;
		} catch (IllegalAccessException e) {
			DispatchingException de = new DispatchingException(e);
			logger.logError(de);
			throw de;
		}
	}

	private static Predicate<Method> getActualParamFilter(
			@NonNullable List<Object> params) {
		List<Class<?>> paramTypes = Lists.transform(params,
				new Function<Object, Class<?>>() {
					@Override
					public Class<?> apply(Object input) {
						return input.getClass();
					}
				});
		return ParamFilters.getParamsTypeFilter(paramTypes, false);
	}

	public static interface IMethodSelectionHandler {
		void noMethodFound() throws NoSuchMethodException;

		boolean canHandleMultiMethods();
	}

	public static final IMethodSelectionHandler DEFAULT_SELECTION_HANDLER = new IMethodSelectionHandler() {
		@Override
		public void noMethodFound() throws NoSuchMethodException {
			throw new NoSuchMethodException();
		}

		@Override
		public boolean canHandleMultiMethods() {
			return false;
		}
	};
}
